"""Module containing LEX/YACC parsing code for the Layyout-variety language.
"""

from ply import lex, yacc
import sys, re
import compiler

reserved = {
    'otherwise': 'OTHERWISE'
}

tokens = (
    'NAME',
    'NUMBER',
    'TEXT',
    'HORIZ',
    'VERT',
    'LPAREN',
    'RPAREN',
    'LSQUARE',
    'RSQUARE',
    'COMMA',
    'PLUS',
    'MINUS',
    'MULT',
    'DIV',
    'INIT',
    'EQUALS',
    'NOT',
    'AND',
    'OR',
    'LCURLY',
    'RCURLY',
    'THEN',
    'ARROW',
    'CROSS',
    'COLON',
    'QMARK',
) + tuple(reserved.values())

t_TEXT    = r'("[^"]*")'
t_HORIZ   = r'---+'
t_VERT    = r'\|'
t_LPAREN  = r'\('
t_RPAREN  = r'\)'
t_LSQUARE = r'\['
t_RSQUARE = r'\]'
t_COMMA   = r','
t_PLUS    = r'\+'
t_MINUS   = r'-'
t_MULT    = r'\*'
t_DIV     = r'/'
t_INIT    = r'=\?'
t_EQUALS  = r'='
t_NOT     = r'!'
t_AND     = r'&&'
t_OR      = r'\|\|'
t_LCURLY  = r'\{'
t_RCURLY  = r'\}'
t_THEN    = r'=>'
t_ARROW   = r'<-'
t_CROSS   = r'@%CROSS%@'
t_COLON   = r':'
t_QMARK   = r'\?'
t_ignore  = ' \t'

tt_NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'
def t_NAME(t):
  r'[a-zA-Z_][a-zA-Z0-9_]*'
  t.type = reserved.get(t.value, 'NAME')
  return t

def t_NUMBER(t):
  r'[0-9]*\.?[0-9]+'
  if "." in t.value:
    t.value = float(t.value)
  else:
    t.value = int(t.value)
  return t

def t_NEWLINE(t):
  r'\n+'
  t.lexer.lineno += t.value.count("\n")

def t_error(t):
  print "Illegal character '%s'" % t.value[0]
  t.lexer.skip(1)

lexer = lex.lex()


from widgets import widget, atoms
import expressions


global_variables = {}

sub_programs = {}
def _get_widget(name):
  for className in dir(atoms):
    if className.lower() == name:
      cls = eval("atoms." + className)
      if issubclass(cls, atoms.Atom):
        return cls
  if name in sub_programs:
    return sub_programs[name]
  return None


NAME_PREFIX = "corwin__"

precedence = (
    ('left', 'ARROW'),
    ('left', 'HORIZ'),
    ('left', 'VERT'),
    ('left', 'EQUALS'),
    ('left', 'AND', 'OR'),
    ('left', 'PLUS', 'MINUS'),
    ('left', 'MULT', 'DIV'),
    ('right', 'NOT'),
)

def p_program(p):
  '''program : program program'''
  p[0] = p[1]
  p[0].update(p[2])

def p_sub_program(p):
  '''program : NAME ARROW composition'''
  p[0] = {p[1]: p[3]}
  sub_programs.update(p[0])

def p_composition(p):
  '''composition : LPAREN composition RPAREN
                 | LPAREN composition RPAREN size
                 | LPAREN composition RPAREN parameters
                 | LPAREN composition RPAREN size parameters'''
  p[0] = p[2]
  if len(p) >= 5 and type(p[4]) == tuple:
    p[0].update_size(*p[4])
    for direction in p[4]:
      if direction is None:
        continue
      for variable in direction.getVariables().itervalues():
        variable.set_require_redraw()
  # We can safely ignore parameters here, since they're already evaluated in the
  # parameter rule. (We don't need to store them for farther use...)

def p_composition_from_compositions(p):
  '''composition : composition VERT composition
                 | composition HORIZ composition'''
  desiredDirection = 0
  if re.match("^" + t_HORIZ + "$", p[2]):
    desiredDirection = 1
  if type(p[1]) == widget.DirectionalComposition and p[1].direction == desiredDirection:
    p[0] = p[1]
    p[0].add_to_list(p[3])
  else:
    p[0] = widget.DirectionalComposition(desiredDirection, defenition_line=p.lineno(1))
    p[0].add_to_list(p[1])
    p[0].add_to_list(p[3])

def p_composition_from_widget(p):
  '''composition : widget'''
  p[0] = p[1]

def p_widget(p):
  '''widget : LPAREN NAME RPAREN
            | LPAREN NAME size RPAREN
            | LPAREN NAME RPAREN parameters
            | LPAREN NAME size RPAREN parameters'''
  p[0] = _get_widget(p[2])
  if p[0] is None:
    print >>sys.stderr, "Undefined widget '%s' at line %s" % (p[2], p.lineno(2))
    raise SyntaxError
  if type(p[0]) == type:
    # This is an atom
    kwargs = {}
    # The idiots didn't implement the -1 index right :(.
    if type(p[len(p)-1]) == dict:
      kwargs = p[len(p)-1]
    p[0] = p[0](defenition_line=p.lineno(2), **kwargs)
    for expression in kwargs.itervalues():
      for variable in expression.getVariables().itervalues():
        variable.add_dependency(p[0].update)
  if type(p[3]) == tuple:
    p[0].update_size(*p[3])
    for direction in p[3]:
      if direction is None:
        continue
      for variable in direction.getVariables().itervalues():
        variable.set_require_redraw()

def p_empty_widget(p):
  '''widget : LPAREN RPAREN
            | LPAREN RPAREN size
            | LPAREN RPAREN parameters
            | LPAREN RPAREN size parameters'''
  kwargs = {}
  # The idiots didn't implement the -1 index right :(.
  if type(p[len(p)-1]) == dict:
    kwargs = p[len(p)-1]
  p[0] = atoms.EmptyBox(defenition_line=p.lineno(1), **kwargs)
  if len(p) >= 4 and type(p[3]) == tuple:
    p[0].update_size(*p[3])
    for direction in p[3]:
      if direction is None:
        continue
      for variable in direction.getVariables().itervalues():
        variable.set_require_redraw()

def p_size(p):
  '''size : COLON size_part CROSS size_part'''
  p[0] = (p[2], p[4])

def p_size_part(p):
  '''size_part : NUMBER
               | QMARK
               | LPAREN expression RPAREN
               | LPAREN condition RPAREN'''
  if re.match("^" + t_QMARK + "$", str(p[1])):
    p[0] = None
  elif len(p) == 2:
    p[0] = expressions.Constant(int(p[1]))
  else:
    expression = to_expression(p[2])
    if expression is None:
      print >>sys.stderr, "Couldn't parse '%s' as a valid expression on line %d." % (p[3], p.lineno(3))
      raise SyntaxError
    p[0] = expression

def p_parameters(p):
  '''parameters : LSQUARE paramlist RSQUARE'''
  p[0] = p[2]

def p_paramlist(p):
  '''paramlist : parameter
               | paramlist COMMA parameter'''
  p[0] = p[1]
  if len(p) == 4:
    p[0].update(p[3])

def create_expression(exp):
  tree = compiler.parse(exp)
  if not isinstance(tree.node, compiler.ast.Stmt):
    return None
  if (len(tree.node.nodes) != 1) or not isinstance(tree.node.nodes[0], compiler.ast.Discard):
    return None
  node = tree.node.nodes[0]
  if len(node.getChildren()) != 1:
    return None
  variables = {}
  has_eq = [False]
  eq_type = [expressions.ExpressionType.SIMPLE]
  def check_exp_and_register_vars(node):
    if node != "==" and not isinstance(node,
        (compiler.ast.Add, compiler.ast.Mul, compiler.ast.Sub, compiler.ast.Div, compiler.ast.Compare,
         compiler.ast.Or, compiler.ast.And, compiler.ast.Not, compiler.ast.Const, compiler.ast.Name,
         compiler.ast.CallFunc)):
      return False
    if isinstance(node, (compiler.ast.Add, compiler.ast.Mul, compiler.ast.Sub, compiler.ast.Div)):
      if eq_type[0] == expressions.ExpressionType.BOOL:
        return False
      eq_type[0] = expressions.ExpressionType.MATH
    elif isinstance(node, (compiler.ast.Or, compiler.ast.And, compiler.ast.Not)):
      if eq_type[0] == expressions.ExpressionType.MATH:
        return False
      eq_type[0] = expressions.ExpressionType.BOOL
    if isinstance(node, compiler.ast.Const):
      return True
    elif node == "==":
      if has_eq[0]:
        return False
      has_eq[0] = True
      return True
    elif isinstance(node, compiler.ast.Name):
      if node.name in variables:
        return True
      elif node.name in global_variables:
        variables[node.name] = global_variables[node.name]
      else:
        variables[node.name] = expressions.Variable()
    elif isinstance(node, compiler.ast.CallFunc):
      for child in xrange(len(node.getChildren())-2):
        if not check_exp_and_register_vars(node.getChildren()[child]):
          return False
    else:
      for child in node.getChildren():
        if not check_exp_and_register_vars(child):
          return False
    return True
  if not check_exp_and_register_vars(node.getChildren()[0]):
    return None
  global global_variables
  global_variables.update(variables)
  return expressions.Expression(exp, variables, eq_type[0], has_eq[0])

def to_expression(expr):
  "Transform text/expression/condition into Expression type"
  if isinstance(expr, expressions.Condition):
    return expr
  try:
    return expressions.Constant(eval(expr, {}, {}))
  except:
    # If we couldn't eval, this is an expression that needs to be interpreted in runtime.
    expr = "==".join("(%s)" % x for x in expr.strip().split("="))
    # Removing parenthesis that scope everything
    lhs = expr.split("==")[0]
    paren_count = lhs.count("(") - lhs.count(")")
    if paren_count:
      expr = expr[paren_count:-paren_count]
    return create_expression(expr)

def p_parameter(p):
  '''parameter : NAME EQUALS text
               | NAME EQUALS expression
               | NAME EQUALS condition
               | NAME INIT LPAREN NUMBER RPAREN
               | NAME INIT LPAREN text RPAREN'''
  p[1] = NAME_PREFIX + p[1]
  if re.match("^" + t_INIT + "$", p[2]):
    if p[1] not in global_variables:
      print "Warning: the variable '%s' initialized at line %d is never used" % (p[1], p.lineno(1))
      p[0] = {}
      return
    if global_variables[p[1]].value is not None:
      print >>sys.stderr, "Redefinition of variable '%s' at line %d" % (p[1], p.lineno(1))
      raise SyntaxError
    global_variables[p[1]].setInitialValue(p[4])
    if type(p[4]) == str:
      global_variables[p[1]].setInitialValue(eval(p[4], {}, {}))
    p[0] = {}
    return
  expression = to_expression(p[3])
  if expression is None:
    print >>sys.stderr, "Couldn't parse '%s' as a valid expression on line %d." % (p[3], p.lineno(3))
    raise SyntaxError
  p[0] = {p[1]: expression}

def p_text(p):
  '''text : TEXT
          | LPAREN text RPAREN'''
  if re.match("^" + t_LPAREN + "$", p[1]):
    p[0] = p[2]
  else:
    p[0] = p[1]

def p_condition(p):
  '''condition : LCURLY cases RCURLY'''
  if not p[2]:
    print >>sys.stderr, "Empty condition bracket on line %d." % p.lineno(1)
    raise SyntaxError
  variables = {}
  default = None
  cond_expr_list = []
  for case in p[2]:
    if isinstance(case, expressions.Expression):
      if default is not None:
        print >>sys.stderr, "Only one default case is allowed in condition on line %d." % p.lineno(1)
        raise SyntaxError
      default = case
      variables.update(case.getVariables())
      continue
    if default is not None:
      print >>sys.stderr, "Default case must be the last case in condition on line %d." % p.lineno(1)
      raise SyntaxError
    cond_expr_list.append(case)
    variables.update(case[0].getVariables())
    variables.update(case[1].getVariables())
  if default is None:
    print >>sys.stderr, "Default case is not present in condition on line %d." % p.lineno(1)
    raise SyntaxError
  p[0] = expressions.Condition(cond_expr_list, default, variables)


def p_cases(p):
  '''cases : case
           | cases COMMA case'''
  if type(p[1]) != list:
    p[0] = [p[1]]
    return
  p[0] = p[1]
  p[0].append(p[3])

def p_case(p):
  '''case : LPAREN expression RPAREN THEN LPAREN expression RPAREN
          | LPAREN expression RPAREN THEN LPAREN text RPAREN
          | LPAREN expression RPAREN THEN LPAREN condition RPAREN
          | LPAREN condition RPAREN THEN LPAREN expression RPAREN
          | LPAREN condition RPAREN THEN LPAREN text RPAREN
          | LPAREN condition RPAREN THEN LPAREN condition RPAREN
          | OTHERWISE LPAREN expression RPAREN
          | OTHERWISE LPAREN text RPAREN
          | OTHERWISE LPAREN condition RPAREN'''
  if p[1] == "otherwise":
    p[0] = to_expression(p[3])
    return
  p[0] = (to_expression(p[2]), to_expression(p[6]))

def p_expression(p):
  '''expression : NAME
                | NAME LPAREN varlist RPAREN
                | NUMBER
                | NOT expression
                | MINUS expression
                | PLUS expression
                | expression PLUS expression
                | expression MINUS expression
                | expression MULT expression
                | expression DIV expression
                | expression AND expression
                | expression OR expression
                | expression EQUALS expression
                | LPAREN expression RPAREN'''
  if len(p) == 2 and type(p[1]) == str and re.match("^" + tt_NAME + "$", p[1]):
    p[1] = NAME_PREFIX + p[1]
  if len(p) == 2:
    p[0] = str(p[1])
  elif len(p) == 3:
    p[0] = (" not " if re.match("^" + t_NOT + "$", p[1]) else p[1]) + p[2]
  elif re.match("^" + t_LPAREN + "$", p[1]):
    p[0] = "(" + p[2] + ")"
  elif len(p) == 5:
    p[0] = p[1] + "(" + p[3] + ")"
  else:
    if re.match("^" + t_AND + "$", p[2]):
      p[0] = p[1] + " and " + p[3]
    elif re.match("^" + t_OR + "$", p[2]):
      p[0] = p[1] + " or " + p[3]
    else:
      p[0] = p[1] + p[2] + p[3]

def p_varlist(p):
  '''varlist : NAME
             | NUMBER
             | varlist COMMA NAME
             | varlist COMMA NUMBER'''
  if type(p[1]) == str and re.mathc("^" + tt_NAME + "$", p[1]):
    p[1] = NAME_PREFIX + p[1]
  p[0] = str(p[1])
  if len(p) == 4:
    if type(p[3]) == str and re.mathc("^" + tt_NAME + "$", p[3]):
      p[3] = NAME_PREFIX + p[3]
    p[0] += "," + str(p[3])

def p_error(p):
  print >>sys.stderr, "Syntax error at %s on line %d" % (repr(p.value), p.lineno)

parser = yacc.yacc(tabmodule="layout_parsetab")

def exchange_x_with_token(code):
  '''This is done, since x can be cross in the size defenition as well as a variable number.
  We'll go over all places where x is the cross, and give it a different name.'''
  in_string = False
  should_skip = False
  in_size = False
  paren_counter = 0
  x_places = []
  for i, c in enumerate(code):
    if in_string:
      if should_skip:
        should_skip = False
        continue
      if c == '\\':
        should_skip = True
        continue
      if c == '"':
        in_string = False
        continue
      continue
    if c == '"':
      in_string = True
      continue
    if c == ':':
      in_size = True
      continue
    if in_size:
      if c == '(':
        paren_counter += 1
        continue
      if c == ')':
        paren_counter -= 1
        continue
      if c == 'x' and paren_counter == 0:
        x_places.append(i)
        in_size = False

  last_x = -1
  new_code = ""
  for x_place in x_places:
    new_code += code[last_x+1:x_place] + t_CROSS
    last_x = x_place
  return new_code + code[last_x+1:]


def parse(code):
  """Parses the input string as Layout variety language.

  Args:
    code: Code which is valid under the Layout variety specifications.
  Returns:
    Dictionary of windows. Each window is of the Widget type.
    If the first "window" of the code has no assignment into a name, the
    default name "main_window" is assigned to it.
  """
  global sub_programs, global_variables
  sub_programs = {}
  global_variables = {}
  # Exchange x with better symbol
  code = exchange_x_with_token(code)
  # Check that we have at least one sub_program here. If not, make the only
  # program be "main_window"
  if code.find("<-") == -1 or not re.match(tt_NAME, code.split("<-", 1)[0].strip()):
    code = "main_window <- " + code
  return parser.parse(code.strip(), lexer=lexer)

